package edu.berkeley.guir.webquilt;

//import edu.berkeley.guir.webquilt.log.*;
import java.io.*;
import java.util.Hashtable;
import java.net.URLEncoder;
import java.util.HashMap;
import java.awt.Rectangle;
import java.util.*;

/**
 * Libraries for WebQuilt.
 *
 *
 * <P>
 * This software is distributed under the
 * <A HREF="http://guir.cs.berkeley.edu/projects/COPYRIGHT.txt">
 * Berkeley Software License</A>.
 *
 * <PRE>
 * Revisions:  - WebQuilt-v1.0-1.0.0, Oct 05 2000, JH
 *               Created class
 *             - WebQuilt-v1.0-1.0.0, Feb 01 2001, jheer
 *               Added query parsing and some stream handling methods
 *             - WebQuilt-v1.0-2.0.0, Mar 10 2001, jheer
 *               Added log handling methods
 *             - Dec 31 2001, kaow
 *               Added Slashdot compatibility and urlIsSpecial()
 *             - 23 Nov 2001 -- sjw
 *               Added more global state stuff
 * </PRE>
 *
 * @author  <A HREF="http://www.cs.berkeley.edu/~jasonh/">Jason Hong</A> (
 *          <A HREF="mailto:jasonh@cs.berkeley.edu">jasonh@cs.berkeley.edu</A>
 *          )
 * @author  Jeffrey Heer (
 *          <A HREF="mailto:jheer@hkn.eecs.berkeley.edu">jheer@hkn.eecs.berkeley.edu</A>
 *          )
 * @author  Sarah Waterson (<A HREF="mailto:waterson@cs.berkeley.edu">waterson@cs.berkeley.edu</A>
 * @author  Wayne Kao (<A HREF="mailto:kaow@cs.berkeley.edu">kaow@cs.berkeley.edu</A>
 * @since   JDK 1.3
 * @version WebQuilt-v1.0-2.0.0, Mar 10 2001
 */
public class WebQuiltLib implements WebQuiltConstants {

	//===========================================================================
	//===   CONSTANTS   =========================================================

	private static final int REMOVE = 0;
	private static final int BLOCK  = 1;
	private static final int KEEP   = 2;

	public static final int DESKTOP = 0;
	public static final int PALM    = 1;
	public static final int PHONE   = 2;

	private static int browserType = DESKTOP;
	private static int browserWidth = DEFAULT_BROWSER_WIDTH;
	private static int browserHeight = DEFAULT_BROWSER_HEIGHT;

	// used to check if we are online or not. we'll assume online as the default
	private static boolean isOnlineFlag = true;

	// used to indicate whether or not we should generate the images from local
	// HTML or go out to the web for the HTML. default is assume to use local
	private static boolean useLocalHTMLFlag = true;

	//used to know if we should use images or just blank blocks
        private static boolean useImages = true;

	//===   CONSTANTS   =========================================================
	//===========================================================================

	//===========================================================================
	//===   ONLINE STATUS   =====================================================
	public static boolean isOnline() {
		return isOnlineFlag;
	} // of method

	public static void setOnline() {
		isOnlineFlag = true;
	} // of method

	public static void setOffline() {
		isOnlineFlag = false;
	} // of method

	public static boolean getUseLocalHTMLFlag() {
		return useLocalHTMLFlag;
	} // of method

	public static void useLocalHTML() {
		useLocalHTMLFlag = true;
	} // of method

	public static void useWebHTML() {
		useLocalHTMLFlag = false;
	} // of method

        public static void setUseImages(boolean b) {
	        useImages = b;
        }// of method

        public static boolean getUseImages() {
	        return useImages;
        } // of method

	//===   ONLINE STATUS   =====================================================
	//===========================================================================


	//===========================================================================
	//===   BROWSER SETTINGS   ==================================================

	/**
	 * Set type of browser:
	 * @param WebQuiltLib.DESKTOP, WebQuiltLib.PALM, or WebQuiltLib.PHONE
	 */
	public static void setBrowserType(int browser) {
		browserType = browser;

		switch (browser) {
		case DESKTOP:
			setBrowserWidth(WIDTH_DESKTOP);
			setBrowserHeight(HEIGHT_DESKTOP);
			break;
		case PALM:
			setBrowserWidth(WIDTH_PALM);
			setBrowserHeight(HEIGHT_PALM);
			break;
		case PHONE:
			setBrowserWidth(WIDTH_PHONE);
			setBrowserHeight(HEIGHT_PHONE);
			break;
		default:
			browserType = DESKTOP;
			setBrowserWidth(WIDTH_DESKTOP);
			setBrowserHeight(HEIGHT_DESKTOP);
		}
	} // of method

	/**
	 * @return browser type: WebQuiltLib.DESKTOP, WebQuiltLib.PALM, or WebQuiltLib.PHONE
	 */
	public static int getBrowserType() {
		return browserType;
	} // of method

	/**
	 * sets the width of the browser screen
	 * @param the width
	 */
	public static void setBrowserWidth(int w) {
		browserWidth = w;
	} // of method

	/**
	 * sets the height of the browser screen
	 * @param the height
	 */
	public static void setBrowserHeight(int h) {
		browserHeight = h;
	} // of method

	/**
	 * gets the width of the browser screen
	 * @return width
	 */
	public static int getBrowserWidth() {
		return browserWidth;
	} // of mehtod

	/**
	 * gets the height of the browser screen
	 * @return height
	 */
	public static int getBrowserHeight() {
		return browserHeight;
	} // of method

	//===   BROWSER SETTINGS   ==================================================
	//===========================================================================

	//===========================================================================
	//===   STREAM READ METHODS   ===============================================

	/**
	 * Reads all data from an input stream into a StringBuffer.
	 * @param istream the input stream
	 * @return the StringBuffer containing the stream data
	 */
	public static byte[] readAllFromInputStream(InputStream istream)
		throws IOException {

		ByteVector   data      = new ByteVector(8192 * 4);
		byte[]       buf       = new byte[8192];
		int          bytesRead = istream.read(buf, 0, 8192);

		while (bytesRead >= 0) {
			data.append(buf, 0, bytesRead);
			bytesRead = istream.read(buf, 0, 8192);
		}

		return (data.toArray());
	} //

	/**
	 * Writes file to an output stream.
	 * @param filename the file to read from
	 * @param ostream the output stream to write to
	 */
	public static void writeFileToOutputStream(String filename,
											   OutputStream ostream)
		throws IOException {
		FileInputStream istream = new FileInputStream(filename);
		connectStreams(istream, ostream);
	} //

	/**
	 * Connects two streams, writing the contents of the input
	 *  stream to the output stream.
	 * @param istream the input stream
	 * @param ostream the output stream
	 */
	public static void connectStreams(InputStream istream, OutputStream ostream)
		throws IOException {

		byte[]       buf = new byte[8192];
		int          bytesRead = istream.read(buf, 0, 8192);

		while (bytesRead >= 0) {
			ostream.write(buf, 0, bytesRead);
			bytesRead = istream.read(buf, 0, 8192);
		}
		ostream.flush();
		ostream.close();
	} //


	//===   STREAM READ METHODS   ===============================================
	//===========================================================================


	//===========================================================================
	//===   URL METHODS   =======================================================

	/**
	 * Given a String of a webquilt URL, trim the "replace" parameter.
	 * @param strReplace the WebQuilt URL to trim
	 * @return the "trimmed" URL
	 */
	public static String trimReplaceParameter(String strReplace) {
		if (strReplace != null) {
			//// Trim spaces.
			strReplace = strReplace.trim();

			//// Trim out trailing '+' characters, what
			////   spaces are transformed into.
			int     i            = strReplace.length() - 1;
			boolean flagTrailing = false;
			while (i >= 0 && strReplace.charAt(i) == '+') {
				i--;
				flagTrailing = true;
			}
			if (flagTrailing == true) {
				strReplace = strReplace.substring(0, i);
			}
		}

		return (strReplace);
	} //


	/**
	 * Given a webquilt "replace" parameter, make sure it's kosher.
	 */
	public static String prepareReplaceParameter(String strReplace) {
		if ( strReplace == null ) { return strReplace; }

		strReplace = strReplace.replace(' ', '+');

		return strReplace;
	} //

	//-----------------------------------------------------------------

	/**
	 * Given a query string, strip out the WebQuilt specific parameters.
	 * @param strQuery the query string to parse
	 * @param params the Hashtable in which to store the WebQuilt parameters
	 * @return the query string with WebQuilt parameters removed
	 */
	public static String stripWebQuiltParams(String strQuery, Hashtable params) {
		//// return empty string if the query string is null
		if ( strQuery == null ) { return ""; }

		//// Now strip each WebQuilt parameter
		for ( int i = 0; i < PARAMS.length; i++ ) {
			strQuery = extractParam(REMOVE, PARAMS[i], strQuery, params);
		}

		//// remove trailing '&', if necessary
		if ( strQuery.endsWith("&") ) {
			strQuery = strQuery.substring(0, strQuery.length() - 1);
		}

		return (strQuery);
	} //


	/**
	 * Given a query string, block out any password fields.
	 * @param strQuery the query string to parse
	 * @return the query string with passwords blocked out
	 */
	public static String blockOutPassword(String strQuery) {

		//// Block out password field(s)
		for ( int i = 0; i < PASSWD.length; i++ ) {
			strQuery = extractParam(BLOCK, PASSWD[i], strQuery, null);
		}

		return strQuery;
	}

	/**
	 * Given a query string, strip out a specific parameter.
	 * @param op one of REMOVE, BLOCK, KEEP. REMOVE removes the parameter from
	 *   the query string, BLOCK replaces the parameter with "*" characters,
	 *   and KEEP leaves it unchanged.
	 * @param strParam the desired parameter
	 * @param strQuery the query string to parse
	 * @param params hashtable of parameter values
	 * @return the query string with WebQuilt parameters removed
	 */
	private static String extractParam(int op, String strParam, String strQuery, Hashtable params) {
		String strPValue;
		int strlen = strQuery.length();

		//// Get start of parameter
		int offset1 = strQuery.indexOf(strParam);

		//// If parameter not in string, continue
		if ( offset1 == -1 ) { return strQuery; }

		//// Get the end of the parameter
		int offset2 = strQuery.indexOf('&', offset1);
		if ( offset2 == -1 ) {
			offset2 = strlen;
		}

		//// Remove the parameter from the query string
		if ( strlen > offset2 && strQuery.charAt(offset2) == '&' ) {
			strPValue = strQuery.substring(offset1, offset2);
			if ( op == REMOVE ) {
				strQuery  = strQuery.substring(0, offset1) +
							strQuery.substring(offset2 + 1);
			} else if ( op == BLOCK ) {
				offset1 += strParam.length() + 1;
				strQuery  = strQuery.substring(0, offset1) + "******" +
							strQuery.substring(offset2);
			}
		} else {
			strPValue = strQuery.substring(offset1);
			if ( op == REMOVE ) {
				strQuery  = strQuery.substring(0, offset1);
			} else if ( op == BLOCK ) {
				offset1 += strParam.length() + 1;
				strQuery = strQuery.substring(0, offset1) + "******";
			}
		}

		//// Extract and return parameter value
		if ( params != null ) {
			strPValue = strPValue.substring(strPValue.indexOf('=') + 1);
			params.put(strParam, strPValue);
		}

		return strQuery;
	}

	/**
	 * Given a URL string, strip off the query string.
	 * @param strUrl the URL
	 * @return the URL with any query string removed
	 */
	public static String stripQuery(String strUrl) {
		int offset = strUrl.indexOf("?");
		if ( offset == -1 ) {
			return strUrl;
		} else {
			return strUrl.substring(0, offset);
		}
	} //

	//-----------------------------------------------------------------

	/**
	 * Given the "replace" url parameter, generate the base href for the page.
	 * @param strReplace the URL string
	 * @return the base HREF for the URL
	 */
	public static String generateBaseHref(String strReplace) {
		int    strlen;
		String strBase = strReplace;

		strlen  = strBase.length();
		if (! (strBase.charAt(strlen - 1) == '/')) {
			int j = strBase.lastIndexOf('/');
			int k = strBase.lastIndexOf('.');
			if (j >= 8 && j < k) {
				strBase = strBase.substring(0, j);
			}
			strBase += '/';
		}
		return (strBase);
	} //

	//------------------------------------------------------------------

        /**
         * Checks for special URL prefixes (e.g. "mailto:" or "javascript:")
         * @param strUrl the URL to analyze
         * @return whether strUrl has a special prefix
         */
         public static boolean urlIsSpecial(String strUrl) {
          return ( strUrl == null                             ||
                  strUrl.equals("")                          ||
                  strUrl.indexOf("mailto:") != -1            ||
                  strUrl.charAt(0) == '#'                    ||
                  strUrl.indexOf("livescript:") != -1        ||
                  strUrl.indexOf("javascript:") != -1        );

        } // of method

	/**
	 * Given a base URL and and a link URL, generate the absolute URL
	 *  for that link.
	 * @param strBase the base URL
	 * @param strLink the link URL
	 * @return an absolute URL to the link
	 */
	public static String generateAbsoluteUrl(String strBase, String strLink) {

		// trim whitespace
		strBase = strBase.trim();
		strLink = strLink.trim();

		if ( startsWithProtocol(strLink) ) {
			return strLink;
		}

		// Slashdot compatibility
		// e.g. <A HREF="//slashdot.org/article.pl?sid=01/1...
		if ( strLink.startsWith("//") ) {

		    // must be http or https
		    return ( strBase.startsWith("https:") ? "https" : "http" ) + strLink;
		}

		if ( strLink.startsWith("/") ) {
			return generateServer(strBase) + strLink.substring(1);

		} else if ( strLink.startsWith("./") || strLink.startsWith("../") ) {
			while ( true ) {
				if ( strLink.startsWith("./") ) {
					strLink = strLink.substring(2);

				} else if ( strLink.startsWith("../")
				        // check if there are anymore directories to strip
					&& strBase.lastIndexOf("/", strBase.length() - 2) != strBase.indexOf("/") + 1 ) {

					strBase = stripLastDir(strBase);
					strLink = strLink.substring(3);
				} else {
					break;
				}
			}
		}

		if (strBase.charAt(strBase.length() - 1) != '/')
			return strBase + '/' + strLink;
		return strBase + strLink;

	} //


	/**
	 * Given a base URL, return the server name
	 * @param strBase the base URL
	 * @return the server specified in the URL
	 */
	public static String generateServer(String strBase) {
		int offset1 = strBase.indexOf("://") + 3;
		int offset2 = strBase.indexOf("/", offset1);
		return strBase.substring(0, offset2+1);
	} //


	/**
	 * Given a base URL, remove the last directory on the string
	 * @param strBase the base URL to strip
	 * @return the URL with the last directory removed
	 */
	public static String stripLastDir(String strBase) {
		int offset = strBase.lastIndexOf("/", strBase.length() - 2);
		return strBase.substring(0, offset+1);
	} //


	/**
	 * Return true if the given URL begins with a protocol
	 * @param strUrl the URL to check
	 * @return true if the URL starts with a protocol, false
	 *          otherwise
	 */
	public static boolean startsWithProtocol(String strUrl) {
		for ( int i = 0; i < CHECKABLE_PROTOCOLS.length; i++ ) {
			if (strUrl.startsWith(CHECKABLE_PROTOCOLS[i])) {
				return true;
			}
		}

		// doesn't start with any of the protocols
		return false;

	   /*
	   now a constant array in WebQuiltConstants -kaow
		return ( strUrl.startsWith("http:")   ||
			 strUrl.startsWith("https:")  ||
			 strUrl.startsWith("ftp:")    ||
			 strUrl.startsWith("gopher:") );
	   */
	} //

	//===   URL METHODS   =======================================================
	//===========================================================================



	//===========================================================================
	//===   LOGGING METHODS   ===================================================

	//// ---------------------------------------------------------
	//// Logging variables - these keep track of where
	////  log files and cached pages belong
	////

	private static String logdir    = "";
	private static String taskdir = "";
	private static String taskDescription = "";
	public static String separator = File.separator;
	private static String strCurrentTaskDir = "";

	//// ---------------------------------------------------------

	/**
	 * Checks a log file directory to make sure it's valid and
	 *  then sets this as the value to be used in the future.
	 *  If a provided directory name is not valid, use the
	 *  current directory.
	 * @param strLogdir the directory name to store log files
	 * @return the final logdir
	 */
	public static void setLogDir(String strLogdir) {
		if ( strLogdir == null || strLogdir.equals("") ) {
			logdir = "";
		}

		if ( !strLogdir.endsWith(separator) ) {
			strLogdir += separator;
		}

		File fileLogdir = new File(strLogdir);
		if ( fileLogdir.exists() ) {
			logdir = strLogdir;
		} else if ( fileLogdir.mkdirs() ) {
			logdir = strLogdir;
		} else {
			logdir = "";
		}

	} //


	/**
	 * Checks a task file directory to make sure it's valid and
	 * then sets this as the value to be used in the future.
	 * If a provided directory name is not valid, use the current directory.
	 * @param strTaskDir the diretory name to store task description files
	 * @return the final taskdir
	 */
	 public static void setTaskDir(String strTaskDir) {
		System.out.println("\n"+strTaskDir);
	    if ( strTaskDir == null || strTaskDir.equals("") ) {
		taskdir = "";
	    }

	    if ( !strTaskDir.endsWith(separator) ) {
		strTaskDir += separator;
	    }

	    File fileTaskDir = new File(strTaskDir);
	    if ( fileTaskDir.exists() ) {
		taskdir = strTaskDir;
	    } else if ( fileTaskDir.mkdirs() ) {
		taskdir = strTaskDir;
	    } else {
		taskdir = "";
	    }
	 } // of method
	/**
	 * Returns the base logfile directory
	 * @return the base logfile directory
	 */
	public static String getLogDir() {
		return ( logdir );
	} //

	/**
	 * Returns the base file directory for the specified task
	 * @param strTaskID the task ID
	 * @return the base file directory for the task
	 */
	public static String getBaseFileName(String strTaskID) {
		return ( logdir + strTaskID + separator );
	} //


	/**
	 * Returns the log file name for a specified task and user.
	 * @param strTaskID the task ID
	 * @param strUserID the user ID
	 * @return the logfile name
	 */
	public static String getLogFileName(String strTaskID, String strUserID) {
		return ( getBaseFileName(strTaskID) + strTaskID + "-" + strUserID + LOGFILE_SUFFIX );
	} //


	/**
	 * Returns the cached file name (ie named of stored copy of proxied
	 *  html, jpg, etc) for the specified task, user, and transaction ID.
	 * @param strTaskID the task ID
	 * @param strUserID the user ID
	 * @param TID the transaction ID
	 * @return the cached file name
	 */
	public static String getCachedFileName(String strTaskID, String strUserID, int TID) {
		return ( getCachedFileDir(strTaskID, strUserID) + TID + ".html" );
	} //


	/**
	 * Returns the directory for cached files
	 * @param strTaskID the task ID
	 * @param strUserID the user ID
	 * @return the directory name cached files go in
	 */
	public static String getCachedFileDir(String strTaskID, String strUserID) {
		return ( getBaseFileName(strTaskID) + strTaskID + "-" + strUserID + separator );
	} //

	/**
	 * returns a string of the task description
	 * @param strTaskID the task ID
	 */
	public static String getTaskDescription(String strTaskID) {
	    //first i'm just going to try and get the damn description in,
	    // then i'll hook them up to the strTaskID's

	    // figure out the file name
		
	    String pathToDesc = new String(taskdir + strTaskID + ".txt");

		System.out.println("\n--->"+taskdir + strTaskID + ".txt");
		
	    String strTaskDesc = new String("");
	    try {
		BufferedReader in  = new BufferedReader(new FileReader(pathToDesc));
		String line = new String("");
		while ( (line=in.readLine()) != null ) {
		    strTaskDesc = strTaskDesc + line;
		}
		in.close();
	    } catch (IOException e) {
                // if no task description, just put in some default text.
                strTaskDesc = new String("No task description available.");
		//e.printStackTrace();
	    }

	    return (strTaskDesc);
	}// of method
	/**
	 * Returns the current task directory
	 */
	 public static String getCurrentTaskDir() {
	    return(strCurrentTaskDir);
	 }// of method

	 /**
	  * Sets the current task directory
	  */
	  public static void setCurrentTaskDir(String dir) {
	    strCurrentTaskDir = dir;
	  } // of method

	//===   LOGGING METHODS   ===================================================
	//===========================================================================

	//===========================================================================
	//===   LINK LOCATION INFO   ================================================

	// Maps to save the link info

	//// (strUrl) --> (HashMap of links-->bounding boxes)
	public static HashMap mapUrlLinks = new HashMap();

	/**
	 * Gets the rectangle bounding box for the indicated link
	 */
	public static Rectangle getLinkLocation(String pageUrl, int linkID){
		//// 1. get the page
		HashMap links = (HashMap) mapUrlLinks.get(pageUrl);
		if (links == null) {
		    //System.err.println("no such page in mapUrlLinks!");
		    return null;
		}

		//// 2. find the correct rectangle
		Rectangle rect = (Rectangle) links.get(new Integer(linkID));
		if (rect == null) {
		    //System.err.println("no such link in hashmap from mapUrlLinks!");
		    return null;
		}

		return (rect);
	}

	// debugging methods
	public static void printLinkInfo(String someUrl) {
	    HashMap mapLinks = (HashMap) mapUrlLinks.get(someUrl);
	    Iterator itURL = mapLinks.keySet().iterator();

	    System.out.println(someUrl);
	    while (itURL.hasNext()) {
		Object linkObj = itURL.next();
		Rectangle rect = (Rectangle) mapLinks.get(linkObj);
		System.out.println(linkObj + ": (" + rect.x + ", " + rect.y + ")");

	    } // while
	}// method

	public static void printLinkInfo() {
		//// 1. grab each url that we've looked at
		Iterator itURL = mapUrlLinks.keySet().iterator();

		while (itURL.hasNext()) {
			//// 2. print out each url
			Object urlObj = itURL.next();
			System.out.println( (String) urlObj );

			//// 3. get the list of links
			HashMap links = (HashMap) mapUrlLinks.get(urlObj);
			Iterator itLinks = links.keySet().iterator();

			//// 4. grab each link
			while (itLinks.hasNext()) {
				Object linkObj = itLinks.next();
				Rectangle rect = (Rectangle) links.get(linkObj);

				//// 5. print it out
				//int id = (int) linkObj;
				System.out.println(linkObj + ": (" + rect.x + ", " + rect.y + ")");
			}
		}

	}

	//===   LINK LOCATION INFO   ================================================
	//===========================================================================

	//===========================================================================
	//===   GLOBAL DATA STRUCTS   ===============================================

	///// some More global data structs to help with filtering
	//public static HashMap mapUserIDFileName = new HashMap();
	//public static HashMap mapFileNameUserID = new HashMap();
	public static HashMap mapUserIDPath = new HashMap();
	public static HashMap mapUserNameFileName = new HashMap();
	public static HashMap mapFileNameUserName = new HashMap();
	public static LinkedList listCurrentUserNames = new LinkedList();
	public static boolean dataStructsBuilt = false;

	/**
	 * Clears state out of all the global data structures:
	 * + mapUrlLinks
	 * + mapUserNameFileName
	 * + mapFileNameUserName
	 * + mapUserIDPath
	 */
	public static void clearDataStructs() {
	    mapUrlLinks = new HashMap();
	    //mapUserNameFileName = new HashMap();
	    //mapFileNameUserName = new HashMap();
	    //mapUserIDPath = new HashMap();
	    //listCurrentUserNames = new LinkedList();
	} // of method

	//===   GLOBAL DATA STRUCTS   ===============================================
	//===========================================================================

} // of class

//==============================================================================

/*
Copyright (c) 2000 Regents of the University of California.
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:

1. Redistributions of source code must retain the above copyright
   notice, this list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright
   notice, this list of conditions and the following disclaimer in the
   documentation and/or other materials provided with the distribution.

3. All advertising materials mentioning features or use of this software
   must display the following acknowledgement:

      This product includes software developed by the Group for User
      Interface Research at the University of California at Berkeley.

4. The name of the University may not be used to endorse or promote products
   derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
*/
